import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.Scanner;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.ArrayList;

public class DataBase {

	private String name;
	private File file;
	private ArrayList<String> attributes;
	private String classAttribute;
	private ArrayList<Sample> samples;
	private ArrayList<Sample> unshuffledSamples;
	private HashMap<String, ArrayList<Sample>> samplesByClass;
	private HashMap<String, ArrayList<Sample>> unshuffledSamplesByClass;

	private int instances;
	private int line;

	@SuppressWarnings("unchecked")
	public DataBase(File file) throws Exception {
		this.file = file;
		this.attributes = new ArrayList<String>();
		this.samples = new ArrayList<Sample>();
		this.samplesByClass = new HashMap<String, ArrayList<Sample>>();

		if (file.getName().endsWith(".dat")) {
			this.loadDatFile(file);
		} else if (file.getName().endsWith(".arff")) {
			this.loadFile(file);
		}
		this.unshuffledSamples = (ArrayList<Sample>) this.samples.clone();
		this.unshuffledSamplesByClass = (HashMap<String, ArrayList<Sample>>) this.samplesByClass.clone();
	}

	public DataBase(String fileName) throws Exception {
		this(new File(fileName));
	}

	public void shuffle() {
		Collections.shuffle(this.samples);

		this.samplesByClass = new HashMap<String, ArrayList<Sample>>();

		for (Sample sample : this.samples) {
			this.addSampleInSamplebyClass(sample);
		}
	}

	public void loadFile(File file) throws Exception {
		this.line = 0;

		try {
			FileInputStream fs = new FileInputStream(file);
			DataInputStream in = new DataInputStream(fs);

			LineNumberReader lineRead = new LineNumberReader(new InputStreamReader(in));
			lineRead.skip(file.length());

			int numLinhas = lineRead.getLineNumber() + 1;

			Scanner scanner = new Scanner(file);

			boolean samples = false;

			while (scanner.hasNextLine()) {
				this.line++;
				String linha = scanner.nextLine();
				StringTokenizer tokenizer = new StringTokenizer(linha);

				while (tokenizer.hasMoreTokens()) {
					String token = tokenizer.nextToken();

					if (!token.startsWith("%")) {
						if (token.toUpperCase().equals("@RELATION")) {
							this.setName(tokenizer.nextToken());
						} else if (token.toUpperCase().equals("@ATTRIBUTE")) {
							this.addAttribute(tokenizer.nextToken());
							break;
						} else if (token.toUpperCase().equals("@DATA")) {
							samples = true;
							this.instances = numLinhas - line - 1;
							break;
						}
					}
				}

				if (samples) {
					break;
				}
			}

			while (scanner.hasNextLine()) {
				this.addSample(scanner.nextLine());
			}
		} catch (Exception e) {
			throw new Exception(String.format("%s: line %d", e.getMessage(), this.line));
		}
	}

	public void loadDatFile(File file) throws Exception {
		this.line = 0;

		try {
			FileInputStream fs = new FileInputStream(file);
			DataInputStream in = new DataInputStream(fs);

			LineNumberReader lineRead = new LineNumberReader(new InputStreamReader(in));
			lineRead.skip(file.length());

			this.instances = lineRead.getLineNumber();
			this.name = getFileNameWithoutExtension();

			Scanner scanner = new Scanner(file);

			int lastAttr = 0;
			while (scanner.hasNextLine()) {
				String[] split = scanner.nextLine().split(":");
				String aux = split[split.length - 2];
				split = aux.split(" ");
				int numero = Integer.parseInt(split[split.length - 1]);
				if (lastAttr < numero) {
					lastAttr = numero;
				}
			}

			for (int i = 0; i < lastAttr; i++) {
				this.addAttribute(i + 1 + "");
			}

			this.addAttribute("class");

			scanner = new Scanner(file);

			while (scanner.hasNextLine()) {
				this.line++;
				String linha = scanner.nextLine();
				this.addDatSample(linha, lastAttr);
			}

		} catch (Exception e) {
			throw new Exception(String.format("%s: line %d", e.getMessage(), this.line));
		}
	}

	public void addAttribute(String attribute) {
		this.attributes.add(attribute);
		this.classAttribute = attribute;
	}

	public void addSample(Sample sample) throws Exception {
		this.samples.add(sample);
		this.addSampleInSamplebyClass(sample);
	}

	public void addSampleInSamplebyClass(Sample sample) {
		ArrayList<Sample> sC = this.samplesByClass.get(sample.getClasse());

		if (sC == null) {
			sC = new ArrayList<Sample>();
			this.samplesByClass.put(sample.getClasse(), sC);
		}

		sC.add(sample);
	}

	public void addSample(String sample) throws Exception {
		StringTokenizer tokenizer = new StringTokenizer(sample, ",");

		if (tokenizer.countTokens() != attributes.size()) {
			throw new Exception(String.format("Invalid sample: attrs sample %d, attrs bd %d", tokenizer.countTokens(),
					attributes.size()));
		}

		Sample samp = new Sample();

		int nTokens = tokenizer.countTokens();
		for (int i = 0; i < nTokens - 1; i++) {
			Double valor = new Double(tokenizer.nextToken());

			samp.addValue(valor);
		}

		String classe = tokenizer.nextToken();
		samp.setClasse(classe);

		this.addSample(samp);
	}

	public void addDatSample(String sample, int nAttrs) throws Exception {
		StringTokenizer tokenizer = new StringTokenizer(sample);

		Sample samp = new Sample(nAttrs);

		samp.setClasse(tokenizer.nextToken());

		int nTokens = tokenizer.countTokens();
		for (int i = 0; i < nTokens; i++) {
			String[] split = tokenizer.nextToken().split(":");
			samp.setValue(Integer.parseInt(split[0]) - 1, new Double(split[1]));
		}

		this.addSample(samp);
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public ArrayList<String> getAttributes() {
		return attributes;
	}

	public void setAttributes(ArrayList<String> attributes) {
		this.attributes = attributes;
	}

	public String getClassAttribute() {
		return classAttribute;
	}

	public void setClassAttribute(String classAttribute) {
		this.classAttribute = classAttribute;
	}

	public void printSamples(String string, ArrayList<Sample> samples) {
		System.out.println(string + "\n");

		System.out.print(classAttribute + "\t");

		for (int i = 0; i < attributes.size() - 1; i++) {
			System.out.print(String.format("%s\t", attributes.get(i)));
		}

		System.out.println();

		for (Sample sample : samples) {
			System.out.println(sample.getClasse() + "\t" + sample);
		}

		System.out.println("\nTotal: " + samples.size());
	}

	public void testClassifier(NearestCentroidClassifier nearestCentroid) {
		System.out.println("Teste de Classificação\n");

		for (int i = 0; i < attributes.size() - 1; i++) {
			System.out.print(String.format("%s\t", attributes.get(i)));
		}

		System.out.println("Class\tPredict Class");

		int errors = 0;

		ArrayList<Sample> testSet = nearestCentroid.getTestSet();

		for (Sample sample : testSet) {
			String estimatedClass = nearestCentroid.classify(sample);

			if (!estimatedClass.equals(sample.getClasse())) {
				errors++;
			}

			System.out.println(sample + "\t" + sample.getClasse() + "\t" + estimatedClass);
		}
		System.out.println(String.format("\nError: %f", errors / (float) testSet.size()));
	}

	public int getInstances() {
		return instances;
	}

	public ArrayList<Sample> getSamples() {
		return samples;
	}

	public void setSamples(ArrayList<Sample> samples) {
		this.samples = samples;
	}

	public HashMap<String, ArrayList<Sample>> getSamplesByClass() {
		return samplesByClass;
	}

	public void setSamplesByClass(HashMap<String, ArrayList<Sample>> samplesByClass) {
		this.samplesByClass = samplesByClass;
	}

	public HashMap<String, ArrayList<Sample>> getUnshuffledSamplesByClass() {
		return unshuffledSamplesByClass;
	}

	public void setUnshuffledSamplesByClass(HashMap<String, ArrayList<Sample>> unshuffledSamplesByClass) {
		this.unshuffledSamplesByClass = unshuffledSamplesByClass;
	}

	public ArrayList<Sample> getUnshuffledSamples() {
		return unshuffledSamples;
	}

	public void setUnshuffledSamples(ArrayList<Sample> unshuffledSamples) {
		this.unshuffledSamples = unshuffledSamples;
	}

	public File writeDataBase(String fileName, Set<Integer> discriminantAttributes, boolean shuffled) throws Exception {

		File file = new File(fileName);

		FileOutputStream saida = new FileOutputStream(file);

		StringBuffer buffer = new StringBuffer("@relation " + name + "\n");

		if (discriminantAttributes == null) {
			for (int i = 0; i < attributes.size() - 1; i++) {
				buffer.append("@attribute " + attributes.get(i) + " Real\n");
			}
		} else {
			for (Integer integer : discriminantAttributes) {
				buffer.append("@attribute " + attributes.get(integer) + " Real\n");
			}
		}

		String strClasses = samplesByClass.keySet().toString();
		strClasses = strClasses.substring(1, strClasses.length() - 1);
		buffer.append("@attribute " + classAttribute + " {" + strClasses + "}\n");// classe
		buffer.append("@data\n");

		ArrayList<Sample> samplesToWrite = (shuffled) ? samples : unshuffledSamples;

		for (Sample sample : samplesToWrite) {
			ArrayList<Double> values = sample.getValues();
			if (discriminantAttributes == null) {
				for (Double value : values) {
					buffer.append(value + ",");
				}
			} else {
				for (Integer integer : discriminantAttributes) {
					buffer.append(values.get(integer) + ",");
				}
			}
			buffer.append(sample.getClasse() + "\n");
		}

		saida.write(buffer.toString().getBytes());
		saida.close();

		return file;
	}

	public File convertDatToArrf() throws Exception {
		return this.writeDataBase(getFilePathWithoutExtension() + ".arff", null, false);
	}

	public File getFile() {
		return file;
	}

	public void setFile(File file) {
		this.file = file;
	}

	public int sizeFileExtension() {
		String fileName = file.getName();

		return fileName.length() - fileName.lastIndexOf(".");
	}

	public String getFilePathWithoutExtension() {
		String fileName = file.getAbsolutePath();

		return fileName.substring(0, fileName.length() - sizeFileExtension());
	}

	public String getFileNameWithoutExtension() {
		String fileName = file.getName();

		return fileName.substring(0, fileName.length() - sizeFileExtension());
	}

	public ArrayList<File> balancedDivision(double trainingRatio) throws Exception {
		ArrayList<File> files = new ArrayList<File>(2);

		ArrayList<Sample> originalSamples = this.samples;
		String originalName = this.name;

		ArrayList<ArrayList<Sample>> sampleGroups = Utils.balancedDivision(this, trainingRatio);

		String fileName = getFilePathWithoutExtension();

		this.setSamples(sampleGroups.get(0));
		this.name = originalName + "Training";
		File trainingFile = this.writeDataBase(fileName + "Training.arff", null, true);
		files.add(trainingFile);

		this.setSamples(sampleGroups.get(1));
		this.name = originalName + "Testing";
		File testingFile = this.writeDataBase(fileName + "Testing.arff", null, true);
		files.add(testingFile);

		this.name = originalName;
		this.samples = originalSamples;

		return files;
	}

	public ArrayList<File> balancedDivision(int n) throws Exception {
		ArrayList<File> files = new ArrayList<File>(2);

		ArrayList<Sample> originalSamples = this.samples;
		String originalName = this.name;

		ArrayList<ArrayList<Sample>> sampleGroups = Utils.balancedDivision(this, n);

		String fileName = getFilePathWithoutExtension();

		for (int i = 0; i < n; i++) {
			this.setSamples(sampleGroups.get(i));
			this.name = originalName + "Part" + i;
			File file = this.writeDataBase(fileName + "Part" + i + ".arff", null, true);
			files.add(file);
		}

		this.name = originalName;
		this.samples = originalSamples;

		return files;
	}
}
